#!/usr/bin/env perl

# Written by Thomas Backman <serenity@exscape.org>
# for Perl 5.10.0 (but without using 5.10 features)
# 2009-02-12 - ?
# New BSD license (do pretty much whatever you want, but credit me!)

# Known limitations:
# Only writes ID3v2 tags (and v2.3 at that)
# As above: can't write ID3v2.4 tags

### TODO ###
# Rename --set to --change, and add a true --set option that sets the selected tag to the last argument. (id3edit.pl -s album *.mp3 'Album name') - create frame if it doesn't exist!
# ID3 v2.4 write support? Probably not possible without MAJOR work (or instability risk) :(
# Make sure of platform compliance (aka. test on Windows, and Perl 5.8.8)
# Config file?
# ...

# id3rawedit:
# ?
### END TODO ###

use warnings;
use strict;
use MP3::Tag;
use Data::Dumper;
use Getopt::Long;
use constant DEBUG => 0;

our $tags = { lyrics => 'USLT', artist => 'TPE1', name => 'TIT2', album => 'TALB', composer => 'TCOM', year => 'TDRC', genre => 'TCON' };

sub print_usage() {
	print STDERR "Usage: $0 <--get tagname/--set tagname> [OPTION]... [FILENAME(S)]... <regex if --set>\n";
	print STDERR "Known tags:\n";
	for my $tag (sort keys %{$tags}) {
		print STDERR "\t$tag\n";
	}
	print STDERR "\n";

		print STDERR "Options can be abbreviated as long as they are unambiguous, i.e. \"--no-new\" would match --no-newline-convert\n";
		print STDERR " -v, --verbose\tPrint info about what we're doing\n";
		print STDERR " -p, --pretend\tNever change the files, just print what we would change\n";
		print STDERR " -g X, --get X\tTag to fetch and print\n";
		print STDERR " -s X, --set X\tTag to fetch, run regex on, and write out\n";
		print STDERR " --no-v2, --no-v2-convert\tDon't convert ID3v1 tags to ID3v2 if v2 tags aren't available\n";
		print STDERR " --no-new, --no-newline-convert\tDon't convert \\r\\n (windows style) newlines to \\n\n";
		print STDERR " \t\t\t\t(these behaviours are never used in --get mode; --get is always read-only)\n";
		print STDERR "\n";

	print STDERR "Examples:\n";
		print STDERR "\t$0 --get lyrics myfile.mp3\n";
		print STDERR "\t$0 --set lyrics myfile.mp3 's/,\\n/\\n/g' (eliminates trailing commas from lines) TEST THIS\n";
		print STDERR "\t$0 --set name *.mp3 's/\$/ (live)/g' (appends \" (live)\" to all track names)\n";
	exit 1;
}

# Initial some variables used throughout the script
my ($verbose, $pretend, $nov2convert, $nonewlineconvert, $get, $set,$regex, $search,$repl,$flags) = (0,0,0,0,0,0,0,0,0,0);
if (scalar @ARGV == 0) {
	print_usage();
}
GetOptions("verbose" => \$verbose, "pretend" => \$pretend, "no-v2-convert" => \$nov2convert, "get=s" => \$get, "set=s" => \$set, "no-newline-convert" => \$nonewlineconvert);

if (DEBUG) {
	print "Verbose: $verbose\n";
	print "Pretend: $pretend\n";
	print "nov2convert: $nov2convert\n";
	print "nonewlineconvert: $nonewlineconvert\n";
	print "Get: $get\n";
	print "Set: $set\n";
}

if ($set) {
	$regex = pop;
	print "Regex: $regex\n" if DEBUG;

	# Make the trailing slash optional if there are no options
	if ($regex !~ m{/$}) {
		$regex = $regex . '/';
	}

	# Split the regex. Lets hope it doesn't contain slashes (which isn't *too* common in filenames!)
	($search, $repl, $flags) = ($1, $2, $3) if $regex =~ m{^s/(.*?)/(.*?)/(\w+)?};
	$repl = "" unless defined $repl;
	$flags = "" unless defined $flags;
	die "Unable to read regex! Make sure it's in the form s/old/new/flags\nFlags are optional, but you might want the /g flag. See man perlre for more info.\n" unless (defined $search && defined $repl);
}

# Some commandline sanity checks
die "You need to use either --get or --set!" unless ($get || $set);
die "You can't both use --get and --set at the same time" if ($get && $set);
die "--set needs a regular expression as the last argument!" if ($set && !$regex);
die "You can't pretend to get, only set!" if ($get && $pretend);
warn "--no-convert isn't needed when using --get; --get is always read-only" if ($get && $nov2convert);

# Define a variable we can use regardless of mode
my $tag = ($get || $set);
unless (defined $tags->{$tag}) {
	die "Unknown tag \"$tag\"!";
}

# Print usage if no files were specified
unless (scalar @ARGV) {
	print_usage();
	exit 1;
}

# Used to hold the old value (the *only* value if --getting)
my $value;
# Used to hold the value of the frame name we *actually* read (i.e. COMM01 even though we asked for COMM)
my $framename;
my $fullframe;

if ($pretend) {
	print "Pretend mode enabled: no changes will be written\n\n";
}

# Loop through all the files specified on the command line
for my $file (@ARGV) {

	if (DEBUG) {
		if ($get) {
			print "Get: $get from $file\n";
		} elsif ($set) {
		# $search, $repl, $flags
			print "Set: $set in $file using s/$search/$repl/$flags\n";
		}
	}

	# These vars are used for ID3v1 tags only, if a v2 tag isn't found and either
	# 1) --get mode is used, or
	# 2) --set mode is used AND auto-conversion to v2 tags isn't disabled
	my ($title, $unused1, $artist, $album, $comment, $year, $unused2);

	my $mp3 = MP3::Tag->new($file);
	unless ($mp3) {
		warn "Unable to open file $file!";
		next;
	}

	unless ($mp3->get_tags()) {
		warn "Unable to read any ID3 tags for file $file";
		next;
	}

	my $id3v2 = undef;
	if (exists $mp3->{ID3v2}) {
		$id3v2 = $mp3->{ID3v2};
		if (defined $id3v2) {
			# Read ID3v2 frame ourselves
			my ($info, $name, @rest);
			($info, $name, @rest) = $id3v2->get_frame($tags->{$tag});
			$framename = $tags->{$tag};
			if ($tag eq "year" && !defined $info) {
				# Retry with the other common year frame
				($info, $name, @rest) = $id3v2->get_frame("TYER");
				if (defined $info) {
					$framename = "TYER";
				}
			}

			# The lyrics frame is one that'll match this:
			if (ref $info) {
				if (exists $info->{Text}) {
					$value = $info->{Text};
					if (!$nonewlineconvert) { 
						$value =~ s/\r\n/\n/g; $value =~ s/\r/\n/g;
					}

					$fullframe = $info;
					$fullframe->{Text} = $value;
				}
			} 
			# This is just a simple tag (like artist or title), just set the value to the first return value
			else {
				$value = $info;
			}
		}
	}
	elsif (!$nov2convert) {

		if (exists $mp3->{ID3v1}) {
			 ($title, $unused1, $artist, $album, $comment, $year, $unused2) = $mp3->autoinfo();

			$value = undef;
			 if ($tag eq "name" && $title) {
				 $value = $title;
			 } elsif ($tag eq "artist" && $artist) {
				 $value = $artist;
			 } elsif ($tag eq "album" && $album) {
				 $value = $album;
			 } elsif ($tag eq "year" && $year) {
				 $value = $year;
			 }
		}
		else {
			print "File $file has no ID3v2 tag nor a ID3v1 file, skipping\n";
			next;
		}
		next unless (defined $value);
	}
	else {
		print "File $file had no ID3v2 tag, and --no-convert specified; skipping file\n";
		next;
	}

	print "TAG: $tag\tFRAME: $framename\n" if DEBUG;

	if ($get && !(ref $value) && $value) {
		print "$value\n";
		next;
	}
	elsif (!$value || !defined $value || length $value < 1) {
		print STDERR "Tag \"$tag\" has no value for file $file\n";
		next;
	}
	elsif ($set) {
		# $search, $repl, $flags
		my $newvalue = $value;
		eval '$newvalue =~ s/$search/' . "$repl/$flags"; # Ugly, but it works!
        die "Unable to execute substitution! Invalid regular expression?\nError message was: $@\n" if $@;

		if ($pretend) {
			print "Tag \"$tag\" for file $file: \"$value\" -> \"$newvalue\"\n";
			next;
		}

		# Set ID3 tag here...
	
		unless (defined $id3v2) {
			unless ($id3v2 = $mp3->new_tag("ID3v2")) {
				print "Unable to create ID3v2 tag for $file\n";
				next;
			}

			# This is needed later, when writing the changed frame to file (remember, we're in --set mode, the tag
			# should have changed! If not, the user messed up the regex.
			$framename = $tags->{$tag};

			my $err = 0;
			$id3v2 = $mp3->{ID3v2} or $err++;
			$id3v2->add_frame($tags->{artist}, $artist) or $err++;
			$id3v2->add_frame($tags->{name}, $title) or $err++;
			$id3v2->add_frame($tags->{album}, $album) or $err++;
			$id3v2->add_frame($tags->{year}, $year) or $err++;

			if ($err == 0 && ($verbose || DEBUG)) {
				print "Added ID3v2 tag for file $file (converted from ID3v1)\n";
			}
			elsif ($err != 0) {
				print "Failed to add ID3v2 tag for file $file\n";
				next;
			}
		}

#		print "framename: $framename tag: $tag tags->tag: $tags->{$tag} newvalue: $newvalue\n" if DEBUG;

		my $newframe;

		if (defined $fullframe && ref $fullframe) {
			$newframe = [ $fullframe->{encoding}, $fullframe->{Language}, $fullframe->{Description}, $newvalue ];
		}

		if (defined $fullframe && ref $fullframe) {

		#unless ($id3v2->remove_frame($framename) && $id3v2->add_frame($framename, @$newframe)) {
		unless ($id3v2->change_frame($framename, @$newframe)) {
				warn "Unable to change tag \"$tag\" for file $file";
				next;
			}
		}
		else {
			unless ($id3v2->change_frame($framename, $newvalue)) {
				warn "Unable to change tag \"$tag\" for file $file";
				next;
			}
		}


		if ($id3v2->write_tag) {
			if ($verbose || DEBUG) {
				if (length $value > 20) { $value = substr($value,0,20) . "..."; }
				print "Changed tag \"$tag\" in $file from \"$value\" to \"$newvalue\"\n"
			}
		}
		else {
			warn "Failed to write tag \"$tag\" in $file\n";
		}
	}
} # end foreach loop
